package com.gambi.tapestry.services;

import java.io.IOException;

import org.apache.tapestry5.Block;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.Invocation;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.MethodAdvice;
import org.apache.tapestry5.ioc.MethodAdviceReceiver;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.InjectService;
import org.apache.tapestry5.ioc.annotations.Match;
import org.apache.tapestry5.runtime.RenderCommand;
import org.apache.tapestry5.services.ComponentClassTransformWorker;
import org.apache.tapestry5.services.Dispatcher;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.LibraryMapping;
import org.apache.tapestry5.services.RequestGlobals;

import com.gambi.tapestry.services.gwt.GWTComponentEventEncryptedResultProcessor;
import com.gambi.tapestry.services.gwt.GWTComponentEventPlainResultProcessor;
import com.gambi.tapestry.services.gwt.GWTComponentEventSource;
import com.gambi.tapestry.services.gwt.GWTEventDispatcher;
import com.gambi.tapestry.services.gwt.GWTOnEventWorker;
import com.gambi.tapestry.services.gwt.GWTRemoteServiceHandler;
import com.gambi.tapestry.services.gwt.GWTScriptDispatcher;
import com.gambi.tapestry.services.gwt.GWTScriptHandler;
import com.gambi.tapestry.services.gwt.GWTServletHttpRequestFilter;
import com.gambi.tapestry.services.gwt.GWTWorker;
import com.gambi.tapestry.services.gwt.ServiceTargetLinkSource;
import com.gambi.tapestry.services.gwt.internal.GWTRemoteServicetHandlerImpl;
import com.gambi.tapestry.services.gwt.internal.GWTScriptHandlerImpl;
import com.gambi.tapestry.services.gwt.internal.ServiceTargetLinkSourceImpl;
import com.gambi.tapestry.utils.GWTConstant;
import com.gambi.tapestry.utils.GWTUtils;

/**
 * This module is automatically included as part of the Tapestry IoC Registry,
 * it's a good place to configure and extend Tapestry, or to place your own
 * service definitions.
 */
public class TapestryGwtModule {

//	public static void contributeComponentClassResolver(
//			Configuration<LibraryMapping> configuration) {
//		configuration.add(new LibraryMapping("tapestry-gwt-core",
//				"com.gambi.tapestry"));
//	}

	/*
	 * Workaround to force the browser to download new version of the assets.
	 * TODO In the next future also core js library if any
	 */
	public static void contributeClasspathAssetAliasManager(
			MappedConfiguration<String, String> configuration) {
		configuration.add("tapestry-gwt-core", "com/gambi/tapestry");
	}

	public static void bind(ServiceBinder binder) {

		/*
		 * REQUEST PROCESSING
		 */

		/*
		 * This filter intercepts the GWT AJAX calls, decode them to RPCRequests
		 * (temporarily saved as Request Attribute).
		 */
		binder.bind(GWTServletHttpRequestFilter.class);

		/*
		 * Service Target Link Source
		 */
		binder.bind(ServiceTargetLinkSource.class,
				ServiceTargetLinkSourceImpl.class);

		/*
		 * If the GWT AJAX Request is for a "real" Tapestry-GWT service, the
		 * service is identified by the URL via prefix and logical page name.
		 * This means that a page must be provided in the right package for each
		 * GwtRemoteService that must be provided. The page must implement the
		 * remote service interface.
		 * 
		 * 
		 * (can be contributed and advised via AoP ? TOOD Controlla se si
		 * possono aggiungere interfacce alle classi e metodi on the fly che
		 * possono anche essere adviced. In questo caso una singola Pagina
		 * VIRTUALE puo fare da HUB a tutti i servizi gwt remoti semplicemente
		 * dichiarando (magari al volo) l'interfaccia che implementa e
		 * aggiungendo i metodi relativi), so the same (identified via the URL)
		 * 
		 * TODO For the moment the binding is fixed. but In the next future a
		 * GWTServiceTargetLinkSource service will be provided to generate
		 * "appropriate" and configurable service end points
		 */
		binder.bind(Dispatcher.class, GWTEventDispatcher.class).withId(
				GWTConstant.GWT_REMOTE_SERVICE);
		/*
		 * The Actual invocation is carried out by the Handler Service.
		 */
		binder.bind(GWTRemoteServiceHandler.class,
				GWTRemoteServicetHandlerImpl.class);

		/*
		 * TODO This set of services have the purpose to provide (as Asset) the
		 * gwt.js libraries, depending on the actual content of the page (i.e.
		 * components) and depending on the declared GWT Modules.
		 */

		/*
		 * TODO Rivedere tutto il meccanismo che non mi e' chiarissimo. Puo
		 * andare in conflitto con le annotazioni che abbiamo gia definito
		 */
		binder.bind(Dispatcher.class, GWTScriptDispatcher.class).withId(
				GWTConstant.GWT_SCRIPT_SERVICE);
		binder.bind(GWTScriptHandler.class, GWTScriptHandlerImpl.class);

		/*
		 * This service tweaks the Tapestry event bubbling to allow one to
		 * return void and boolean types on demand.
		 */
		binder.bind(GWTComponentEventPlainResultProcessor.class);
		binder.bind(GWTComponentEventEncryptedResultProcessor.class);

		/*
		 * This service tweaks the mapping between components and their declared
		 * events during the class transformation. This is used to correctly
		 * identify the gwt-events handler w.r.t. the tapestry event handlers.
		 */
		binder.bind(GWTComponentEventSource.class);
	}

	public void contributeMasterDispatcher(
			OrderedConfiguration<Dispatcher> configuration,
			@InjectService(GWTConstant.GWT_REMOTE_SERVICE) Dispatcher gwtrpc,
			@InjectService(GWTConstant.GWT_SCRIPT_SERVICE) Dispatcher gwtScript) {

		// It must contribute before the ComponentEvent to prevent the
		// triggering of all the machinery of Tapestry.. that will restul in an
		// exception somewhere :)
		configuration.add(GWTConstant.GWT_REMOTE_SERVICE, gwtrpc,
				"before:ComponentEvent");

		// It must contribute before the asset service otherwise all the gwt.js
		// files
		// will be treated as "normal" assets.
		configuration.add(GWTConstant.GWT_SCRIPT_SERVICE, gwtScript,
				"before:Asset");

	}

	public void contributeHttpServletRequestHandler(
			OrderedConfiguration<HttpServletRequestFilter> configuration,
			GWTServletHttpRequestFilter filter) {

		// In the standard Tapestry configuration we can put the gwt http
		// servlet as the last one
		// so we can have for free gzip compression and the other feature to
		// manage the low levels of the transmission and request.
		configuration.add(GWTConstant.GWT_HTTP_SERVLET_FILTER_SERVICE_ID,
				filter, "after:*");
	}

	public static void contributeComponentClassTransformWorker(
			OrderedConfiguration<ComponentClassTransformWorker> configuration) {
		/*
		 * Wrap the original OnEvent Transformations with the GWT one. This may
		 * be unsafe if the logic of the Original Worker changes...
		 * 
		 * In any case we need to let our worker to take care of the OnEvent
		 * Processing
		 */
		configuration.overrideInstance("OnEvent", GWTOnEventWorker.class,
				new String[] {});

		/*
		 * Worker to process the class level and field level annotation. It
		 * depends on the previous worker because it needs to retrieve the ful
		 * list of component events that are also GWT enabled.
		 * 
		 * TODO The right way to do that is to use the Service Decoration on top
		 * the original event instead of using its code :)
		 * 
		 * NOTE: Must be after the ParameterWorker to be sure all the Paramter
		 * annotations on component fields are processed. Morever, there is a
		 * "strong" dependency between ParameterWorker and BindParameterWorker
		 * that is they MUST be one after the other. This implies that we need
		 * to put this at least after the BindPatameter
		 * 
		 * With after:BindParameter this screww up the rendering process (dunno
		 * why)
		 */
		configuration.addInstance(GWTConstant.GWT_WORKER, GWTWorker.class,
				"after:BindParameter");

	}

	// TODO This must be reviewed... Basically it adds the final query
	// parameters that is needed by GWTto identify the hosted mode.
	// if implemented fine (not triviall) this is really nice feature to have
	// while developing the application. It should be fine to add the query
	// paraemter in any case, just because it is simply ignored by all the
	// tapestry components... Need some verification. MAYBE Tapestry 5.2 has a
	// different way to do URL rewriting... and maybe this can be used in
	// conjuction
	// to the ServiceLinkSource stuff to create on the fly the service end
	// points
	// and map them to the right tapestry pages... Interesting '.'

	/*
	 * This will add the missing part when we run the application as HOSTED
	 * mode. TODO Maybe this is something useful to bind to the
	 * Tapestry.ProductionMode symbol
	 */
	// Remember to launch your app with the -DcodeServer=127.0.0.1:9997
	// static final String codeServer = System.getProperty("codeServer");
	// public static void contributeURLRewriter(
	// OrderedConfiguration<URLRewriterRule> configuration) {
	//
	// if (codeServer != null) {
	// URLRewriterRule rule = new URLRewriterRule() {
	//
	// public RewriteRuleApplicability applicability() {
	// return RewriteRuleApplicability.OUTBOUND;
	// }
	//
	// public Request process(Request request,
	// URLRewriteContext context) {
	// // if this is a component event, getPageParameters() will
	// // // return null.
	// if (context.getPageParameters() == null)
	// return request;
	// // not really right but good enough for dev
	// String path = request.getPath() + "?gwt.codesvr="
	// + codeServer;
	// return new SimpleRequestWrapper(request, path);
	// }
	// };
	// configuration.add("rule1", rule);
	// }
	// }

	@Match("*ComponentEventResultProcessor")
	public static void adviseGwtResult(
			final MethodAdviceReceiver receiver,
			final RequestGlobals requestGlobals,
			final GWTComponentEventPlainResultProcessor gwtComponentEventPlainResultProcessor,
			final GWTComponentEventEncryptedResultProcessor gwtComponentEventResultEncryptedProcessor) {

		MethodAdvice advice = new MethodAdvice() {

			public void advise(Invocation invocation) {
				// Check for the requestContent
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getMethodName());
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getParameterCount());
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getParameterType(0));
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getParameter(0));
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getResult());
				// System.out
				// .println("AppModule.adviseGwtResult(...).new MethodAdvice() {...}.advise() "
				// + invocation.getResultType());

				if (GWTUtils.isGwtEncryptedRequest(requestGlobals
						.getHTTPServletRequest())) {

					/*
					 * TODO
					 * 
					 * Here is still missing a way to identify truly String
					 * return type and return types that come from the Tapestry
					 * way of doing things: Link -> Page class -> Page name
					 * (string) -> JSONObject.
					 * 
					 * What is also missing it is an idea on how to deal with
					 * redirect contentType. At the moment I simply encode the
					 * link assuming that the GWT component knows how to deal
					 * with that.
					 */

					// TODO Note that Block and RenderCommand are unknown
					// objects to GWT.. so here we assume
					// that the plain response, an JSON with a stanrdar
					// attributes ans content will be generated and passed along
					// the line
					// The trick consists in letting our filter to encode the
					// payload late in the chain. This because the Block
					// Processor
					// open the stream of the Response object and writes the
					// content directly to it !!!

					if (invocation.getParameter(0) instanceof Block) {
						// Default: convert to String the rendered block ?
						invocation.proceed();
					} else if (invocation.getParameter(0) instanceof RenderCommand) {
						// Default: convert to String ?
						invocation.proceed();
					}

					else {
						try {
							// This must be the invocation to the
							gwtComponentEventResultEncryptedProcessor
									.processResultValue(invocation
											.getParameter(0));

						} catch (IOException e) {
							e.printStackTrace();
							throw new RuntimeException(e);
						}
					}

				} else {
					// here we need to identify if the request is related to a
					// bubbling-blocking method

					if (GWTUtils
							.isGwtEventBubblingBlockinRequest(requestGlobals
									.getHTTPServletRequest())) {
						try {
							// This must be the invocation to the
							gwtComponentEventPlainResultProcessor
									.processResultValue(invocation
											.getParameter(0));

						} catch (IOException e) {
							e.printStackTrace();
							throw new RuntimeException(e);
						}
					} else {
						// This is the original invocation... No gwt enabled
						invocation.proceed();
					}
				}
			}
		};
		receiver.adviseAllMethods(advice);
	};

	// This was the old version ... maybe there are still some use cases that
	// require it.
	// Basically instead of encoding the gwt parameters as RequestPatameters one
	// can use the
	// Tapestry URL encoding and (internally) modify the request URL by adding
	// those info as event context
	// and then re-use the normal tapestry component event handling mechanisms.
	// The drawback is that for all the classes one have to specify the
	// coercion... or have to specify some kind of service
	// like the hibernate one to put in the URL some kind of ID to get back
	// later the original object.
	// /*
	// * THIS IS USED TO Trasform gwt parameters to Context events in the URL,
	// may
	// * be not needed anymore if we extract parameters to request parameters
	// */
	// public static void contributeTypeCoercer(
	// Configuration<CoercionTuple> configuration) {
	//
	// configuration.add(new CoercionTuple<String, Contact>(String.class,
	// Contact.class, new Coercion<String, Contact>() {
	//
	// public Contact coerce(String input) {
	// return new Contact(input);
	// }
	//
	// }));
	//
	// configuration.add(new CoercionTuple<Contact, String>(Contact.class,
	// String.class, new Coercion<Contact, String>() {
	//
	// public String coerce(Contact input) {
	// return input.getName();
	// }
	//
	// }));
	//
	// configuration.add(new CoercionTuple<Message, String>(Message.class,
	// String.class, new Coercion<Message, String>() {
	//
	// public String coerce(Message input) {
	// return input.toString();
	// }
	//
	// }));
	//
	// configuration.add(new CoercionTuple<String, Message>(String.class,
	// Message.class, new Coercion<String, Message>() {
	//
	// public Message coerce(String input) {
	// return new Message(input);
	// }
	//
	// }));
	// }
}
